package com.cloudfun.campusshare.code.generate;

import com.cloudfun.campusshare.code.generate.definition.*;
import com.cloudfun.campusshare.common.security.AuthRequired;
import com.cloudfun.campusshare.entity.*;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.hibernate.annotations.Table;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.persistence.Column;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.*;

/**
 * Created by Huqin on 2020/5/26
 */
@Slf4j
public class CodeGenerator {

    public static final String SRC_CODE_FOLDER = "D:\\campusshare\\src\\main\\java\\";

    public static final Set<String> IGNORE_ENTITY_FIELDS = Sets.newHashSet("createTime", "updateTime", "version");

    public static final JavaFileClassLoader JAVA_FILE_CLASS_LOADER = new JavaFileClassLoader();

    public static void main(String[] args) {
        // generate(UserCouponEntity.class);
    }

    public static void generate(Class clazz) {
        Map<JavaFileDefType, JavaFileDefinition> typeDefMap = new HashMap<>();
        genJavaFile(genRepoDefinition(clazz, typeDefMap));
        genJavaFile(genDTODefinition(clazz, typeDefMap));
        genJavaFile(genVODefinition(clazz, typeDefMap));
        genJavaFile(genListVODefinition(clazz, typeDefMap));
        genJavaFile(genServiceDefinition(clazz, typeDefMap));
        genJavaFile(genServiceImplDefinition(clazz, typeDefMap));
        genJavaFile(genController(clazz, typeDefMap));
    }

    @SneakyThrows
    public static JavaFileDefinition genController(Class entityClazz, Map<JavaFileDefType, JavaFileDefinition> typeDefMap) {
        JavaFileDefinition build = new JavaFileDefinition(JavaFileDefType.CONTROLLER, false, entityClazz);

        build.addAnnotation(new AnnotationDefinition(RestController.class));
        build.addAnnotation(new AnnotationDefinition(AuthRequired.class));
        AnnotationDefinition reqMapping = new AnnotationDefinition(RequestMapping.class);
        reqMapping.addAttr("value", "/v1/" + StringUtils.capitalize(JavaFileDefinition.getEntityOriginName(entityClazz)));
        build.addAnnotation(reqMapping);
        AnnotationDefinition api = genAnnotationDef(entityClazz, Api.class, JavaFileDefType.CONTROLLER.getDesc());
        build.addAnnotation(api);

        FieldDefinition fieldDefinition = new FieldDefinition();
        fieldDefinition.setName(StringUtils.uncapitalize(typeDefMap.get(JavaFileDefType.SERVICE).getSimpleClassName()));
        fieldDefinition.setClazz(JAVA_FILE_CLASS_LOADER.loadClass(typeDefMap.get(JavaFileDefType.SERVICE).getClassName()));
        fieldDefinition.addAnnotation(new AnnotationDefinition(Autowired.class));
        build.addField(fieldDefinition);

        build.init();
        typeDefMap.put(build.getType(), build);
        return build;
    }

    @SneakyThrows
    public static JavaFileDefinition genServiceImplDefinition(Class entityClazz, Map<JavaFileDefType, JavaFileDefinition> typeDefMap) {
        JavaFileDefinition build = new JavaFileDefinition(JavaFileDefType.SERVICE_IMPL, false, entityClazz);

        build.addAnnotation(new AnnotationDefinition(Service.class));
        build.setImplClasses(Arrays.asList(typeDefMap.get(JavaFileDefType.SERVICE).getClassName()));

        FieldDefinition fieldDefinition = new FieldDefinition();
        fieldDefinition.setName(StringUtils.uncapitalize(typeDefMap.get(JavaFileDefType.REPO).getSimpleClassName()));
        fieldDefinition.setClazz(JAVA_FILE_CLASS_LOADER.loadClass(typeDefMap.get(JavaFileDefType.REPO).getClassName()));
        fieldDefinition.addAnnotation(new AnnotationDefinition(Autowired.class));
        build.addField(fieldDefinition);

        MethodDefinition create = new MethodDefinition(build);
        create.setRetClazz(Void.class);
        create.setMethodName("create");
        create.setParameters(Lists.newArrayList(JAVA_FILE_CLASS_LOADER.loadClass(typeDefMap.get(JavaFileDefType.DTO).getClassName())));
        // TODO
        create.setBody("");
        build.addMethod(create);

        MethodDefinition modify = new MethodDefinition(build);
        modify.setRetClazz(Void.class);
        modify.setMethodName("modify");
        modify.setParameters(Lists.newArrayList(JAVA_FILE_CLASS_LOADER.loadClass(typeDefMap.get(JavaFileDefType.DTO).getClassName())));
        // TODO
        modify.setBody("");
        build.addMethod(modify);

        MethodDefinition delete = new MethodDefinition(build);
        delete.setRetClazz(Void.class);
        delete.setMethodName("delete");
        delete.setParameters(Lists.newArrayList(String.class));
        // TODO
        delete.setBody("");
        build.addMethod(delete);

        build.init();
        typeDefMap.put(build.getType(), build);
        return build;
    }

    @SneakyThrows
    public static JavaFileDefinition genServiceDefinition(Class entityClazz, Map<JavaFileDefType, JavaFileDefinition> typeDefMap) {
        JavaFileDefinition build = new JavaFileDefinition(JavaFileDefType.SERVICE, true, entityClazz);

        MethodDefinition create = new MethodDefinition(build);
        create.setRetClazz(Void.class);
        create.setMethodName("create");
        create.setParameters(Lists.newArrayList(JAVA_FILE_CLASS_LOADER.loadClass(typeDefMap.get(JavaFileDefType.DTO).getClassName())));
        create.setMethodCommentDefinition(new MethodCommentDefinition("创建", create));
        build.addMethod(create);

        MethodDefinition modify = new MethodDefinition(build);
        modify.setRetClazz(Void.class);
        modify.setMethodName("modify");
        modify.setParameters(Lists.newArrayList(JAVA_FILE_CLASS_LOADER.loadClass(typeDefMap.get(JavaFileDefType.DTO).getClassName())));
        modify.setMethodCommentDefinition(new MethodCommentDefinition("修改", modify));
        build.addMethod(modify);

        MethodDefinition delete = new MethodDefinition(build);
        delete.setRetClazz(Void.class);
        delete.setMethodName("delete");
        delete.setParameters(Lists.newArrayList(String.class));
        delete.setMethodCommentDefinition(new MethodCommentDefinition("删除", delete));
        build.addMethod(delete);

//        JavaFileDefinition.MethodDefinition list = build.new MethodDefinition();
//        list.setRetClazz(Void.class);
//        list.setMethodName("search");
//        list.setParameters(Lists.newArrayList(String.class));
//        // TODO
//        list.setBody("");
        build.init();
        typeDefMap.put(build.getType(), build);
        return build;
    }

    public static JavaFileDefinition genDTODefinition(Class entityClazz, Map<JavaFileDefType, JavaFileDefinition> typeDefMap) {
        JavaFileDefinition res = new JavaFileDefinition(JavaFileDefType.DTO, false, entityClazz);

        res.addAnnotation(genAnnotationDef(entityClazz, ApiModel.class, JavaFileDefType.DTO.getDesc()));

        res.setFields(getFieldDefinitions(entityClazz));
        res.init();
        typeDefMap.put(res.getType(), res);
        return res;
    }

    public static JavaFileDefinition genVODefinition(Class entityClazz, Map<JavaFileDefType, JavaFileDefinition> typeDefMap) {
        JavaFileDefinition res = new JavaFileDefinition(JavaFileDefType.VO, false, entityClazz);

        res.addAnnotation(genAnnotationDef(entityClazz, ApiModel.class, JavaFileDefType.VO.getDesc()));

        res.setFields(getFieldDefinitions(entityClazz));
        res.init();
        typeDefMap.put(res.getType(), res);
        return res;
    }

    public static JavaFileDefinition genListVODefinition(Class entityClazz, Map<JavaFileDefType, JavaFileDefinition> typeDefMap) {
        JavaFileDefinition res = new JavaFileDefinition(JavaFileDefType.LIST_VO, false, entityClazz);

        res.addAnnotation(genAnnotationDef(entityClazz, ApiModel.class, JavaFileDefType.LIST_VO.getDesc()));

        res.setFields(getFieldDefinitions(entityClazz));
        res.init();
        typeDefMap.put(res.getType(), res);
        return res;
    }

    public static JavaFileDefinition genRepoDefinition(Class entityClazz, Map<JavaFileDefType, JavaFileDefinition> typeDefMap) {
        JavaFileDefinition build = new JavaFileDefinition(JavaFileDefType.REPO, true, entityClazz);

        build.setImports(Sets.newHashSet(entityClazz, PagingAndSortingRepository.class, JpaSpecificationExecutor.class));

        build.setImplClasses(Lists.newArrayList("org.springframework.data.repository.PagingAndSortingRepository<" + entityClazz.getSimpleName() + ", String>",
                "org.springframework.data.jpa.repository.JpaSpecificationExecutor<" + entityClazz.getSimpleName() + ">"));
        build.init();
        typeDefMap.put(build.getType(), build);
        return build;
    }

    @SneakyThrows
    private static void genJavaFile(JavaFileDefinition javaFileDefinition) {
        String content = javaFileDefinition.toString();

        File file = new File(javaFileDefinition.getFile());
        log.debug("generate java file：{}", javaFileDefinition.getFile());
        writeToFile(content, file);
    }

    private static void writeToFile(String content, File file) {
        try (FileOutputStream fileOutputStream = new FileOutputStream(file)) {
            IOUtils.write(content, fileOutputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static List<FieldDefinition> getFieldDefinitions(Class clazz) {
        List<FieldDefinition> res = new ArrayList<>();
        List<Field> fields = getFields(clazz);
        for (Field field : fields) {
            if (IGNORE_ENTITY_FIELDS.contains(field.getName())) {
                continue;
            }
            FieldDefinition fieldDefinition = new FieldDefinition();
            AnnotationDefinition apiModelProperty = new AnnotationDefinition(ApiModelProperty.class);
            Column column = field.getAnnotation(Column.class);
            if (null != column) {
                String columnDefinition = column.columnDefinition();
                if (columnDefinition.contains("'")) {
                    String comment = columnDefinition.substring(columnDefinition.indexOf("'") + 1).replace("'", "");
                    apiModelProperty.addAttr("value", comment);
                }
            }
            fieldDefinition.addAnnotation(apiModelProperty);
            fieldDefinition.setClazz(field.getType());
            fieldDefinition.setName(field.getName());
            res.add(fieldDefinition);
        }
        return res;
    }

    private static List<Field> getFields(Class clazz) {
        Set<String> fieldNames = new HashSet<>();
        List<Field> res = new ArrayList<>();
        Class curClazz = clazz;
        while (true) {
            Field[] declaredFields = curClazz.getDeclaredFields();
            for (Field declaredField : declaredFields) {
                if (!fieldNames.contains(declaredField.getName())) {
                    res.add(declaredField);
                    fieldNames.add(declaredField.getName());
                }
            }
            curClazz = curClazz.getSuperclass();
            if (null == curClazz || curClazz.equals(BaseEntity.class)) {
                break;
            }
        }
        return res;
    }

    private static AnnotationDefinition genAnnotationDef(Class entityClazz, Class annotation, String addInfo) {
        AnnotationDefinition annotationDefinition = new AnnotationDefinition(annotation);
        Table table = ((Table) entityClazz.getAnnotation(Table.class));
        if (null != table) {
            String tableComment = table.comment();
            annotationDefinition.addAttr("value", tableComment + addInfo);
        }
        return annotationDefinition;
    }

}
